<script setup lang="ts">
import { onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { TransformControls } from 'three/addons/controls/TransformControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'
import { editing } from '../../store/store.js'
import { ElTag } from 'element-plus'
import { staticUrl } from '../../request/request'

const props = defineProps(['option', 'infoData'])
const modelRef = ref()
const emit = defineEmits(['select'])
const currentObject = ref()
const title = reactive({
  value: '',
  x: 0,
  y: 0
})

const pointer = ref(false)

let width = 300
let height = 200

const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
const infoBoxRenderer = new CSS2DRenderer()
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 100000)
const scene = new THREE.Scene()
const directionalLight = new THREE.DirectionalLight(0xffffff, 5)
const ambientLight = new THREE.AmbientLight(0x404040, 1)
const directionalLightShadowHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
const orbitControls = new OrbitControls(camera, renderer.domElement)
const transformControls = new TransformControls(camera, renderer.domElement)
const clock = new THREE.Clock()
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2(1, 1)
const mixers = []
const modelObjs = []
const loader = new GLTFLoader()
const updateTransformParams = ref() // 触发变换输入框更新

// 更新model变换
const updateModelMatrix = () => {
  if (!currentObject.value) return

  currentObject.value.userData.model.matrix = currentObject.value.matrix.toArray()
}
// 变换控制器事件
transformControls.addEventListener('dragging-changed', function (event) {
  orbitControls.enabled = !event.value

  if (!event.value) updateModelMatrix()
})

transformControls.addEventListener('change', function (event) {
  updateTransformParams.value = Date.now()
})

// 变换控制器目标
function transformControlsObject(obj) {
  transformControls.attach(obj)
  const gizmo = transformControls.getHelper()
  scene.add(gizmo)
}
// 选中模型
function selectObject(obj) {
  if (props.option.transformControls.enabled) {
    transformControlsObject(obj)
    currentObject.value = obj

    emit('select', obj.userData.model)
  }
}

function initCamera() {
  camera.aspect = width / height
  camera.position.set(...props.option.camera.position)
  camera.updateProjectionMatrix()
}

function initDirectionalLight() {
  directionalLight.color.set(props.option.directionalLight.color)
  directionalLight.intensity = props.option.directionalLight.intensity
  directionalLight.position.set(...props.option.directionalLight.position)
  directionalLight.castShadow = props.option.directionalLight.castShadow
  const d = props.option.directionalLight.shadowCameraRadius
  directionalLight.shadow.mapSize.width = 2048
  directionalLight.shadow.mapSize.height = 2048
  directionalLight.shadow.camera.left = -d
  directionalLight.shadow.camera.right = d
  directionalLight.shadow.camera.top = d
  directionalLight.shadow.camera.bottom = -d
  directionalLight.shadow.camera.far = 3500
  directionalLight.shadow.camera.updateProjectionMatrix()
  directionalLight.shadow.bias = -0.0001

  directionalLightShadowHelper.update()
  directionalLightShadowHelper.visible = props.option.directionalLight.shadowHelperVisible
}

function initAmbientLight() {
  ambientLight.color.set(props.option.ambientLight.color)
  ambientLight.intensity = props.option.ambientLight.intensity
}

function initOrbitControls() {
  orbitControls.target = new THREE.Vector3(...props.option.orbitControls.target)
}

function initTransformControls() {
  const { enabled, mode } = props.option.transformControls

  transformControls.enabled = enabled
  transformControls.setMode(mode)
  transformControls.setSize(0.6)

  if (!enabled) transformControls.detach()
}

watch(() => props.option.camera, initCamera, { deep: true })
watch(() => props.option.directionalLight, initDirectionalLight, { deep: true })
watch(() => props.option.ambientLight, initAmbientLight, { deep: true })
watch(() => props.option.orbitControls, initOrbitControls, { deep: true })
watch(() => props.option.transformControls, initTransformControls, { deep: true })
// 初始化环境
function init() {
  if (!modelRef.value) return

  initCamera()
  initDirectionalLight()
  initAmbientLight()
  initOrbitControls()
  initTransformControls()

  renderer.setClearAlpha(0)
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(width, height)
  renderer.setAnimationLoop(animate)
  renderer.shadowMap.enabled = true
  modelRef.value.appendChild(renderer.domElement)

  infoBoxRenderer.setSize(width, height)
  infoBoxRenderer.domElement.style.position = 'absolute'
  infoBoxRenderer.domElement.style.top = '0px'
  infoBoxRenderer.domElement.style.pointerEvents = 'none'
  modelRef.value.appendChild(infoBoxRenderer.domElement)

  const size = getComputedStyle(modelRef.value)
  width = Number.parseFloat(size.width) || 300
  height = Number.parseFloat(size.height) || 200

  const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x888888, 0.2)
  hemisphereLight.position.set(0, 1, 0)
  scene.add(hemisphereLight)

  scene.add(directionalLight)
  scene.add(directionalLightShadowHelper)
  scene.add(ambientLight)

  orbitControls.enableDamping = true
  orbitControls.enableZoom = true
  orbitControls.enablePan = true
  orbitControls.maxPolarAngle = Math.PI / 2
}

const resizeObserver = new ResizeObserver((entries) => {
  onResize()
})

function onResize() {
  if (!modelRef.value) return

  const size = getComputedStyle(modelRef.value)
  width = Number.parseFloat(size.width) || 300
  height = Number.parseFloat(size.height) || 200
  camera.aspect = width / height
  camera.updateProjectionMatrix()
  renderer.setSize(width, height)
  infoBoxRenderer.setSize(width, height)
}

const getRayTarget = () => {
  raycaster.setFromCamera(mouse, camera)

  const objects = []

  scene.traverseVisible(function (child) {
    if (!child.isTransformControlsPlane) objects.push(child)
  })

  const intersection = raycaster.intersectObjects(objects, false)

  if (intersection.length > 0) {
    return intersection[0].object
  }
}

function onMouseMove(event) {
  // mouse.x = (event.offsetX / width) * 2 - 1
  // mouse.y = -(event.offsetY / height) * 2 + 1
  // const offset = 10
  // const obj = getRayTarget()
  // if (obj) {
  //   title.value = obj.userData.model.name
  //   title.x = event.offsetX + offset
  //   title.y = event.offsetY + offset
  //   pointer.value = true
  // } else {
  //   pointer.value = false
  // }
}
// down事件跟变化控制器冲突
// 点击事件
function onmouseup(event) {
  mouse.x = (event.offsetX / width) * 2 - 1
  mouse.y = -(event.offsetY / height) * 2 + 1

  const obj = getRayTarget()

  if (obj && obj.userData.whole) {
    const whole = obj.userData.whole
    // 切换信息框显隐
    if (whole.userData.infoBox) whole.userData.infoBox.visible = !whole.userData.infoBox.visible

    selectObject(whole)
  }
}

// 加载模型
const cache = new Map() // 模型缓存

const loadModel = async (model) => {
  if (!model.url || typeof model.url !== 'string') return

  return new Promise((resolve, reject) => {
    loader.load(staticUrl(model.url), function (gltf) {
      gltf.scene.traverse((obj) => {
        obj.castShadow = true
        obj.receiveShadow = true
        obj.userData.model = model // 整体模型
        obj.userData.whole = gltf.scene
      })

      if (model.matrix) {
        const trans = new THREE.Matrix4()
        trans.fromArray(model.matrix)
        gltf.scene.applyMatrix4(trans)
      }

      scene.add(gltf.scene)
      modelObjs.push(gltf.scene)
      cache.set(model.name, {
        url: model.url,
        mesh: gltf.scene
      })

      if (gltf.animations.length > 0) {
        const mixer = new THREE.AnimationMixer(gltf.scene)
        mixer.clipAction(gltf.animations[0]).play()
        mixers.push(mixer)
      }

      resolve(gltf.scene)
    })
  })
}

const initModels = async () => {
  scene.remove(...modelObjs)

  let toDetach = true

  const all = props.option.models.map((model) => {
    if (cache.get(model.name)?.url === model.url) {
      const object = cache.get(model.name).mesh
      scene.add(object)

      if (object == transformControls.object) toDetach = false

      return
    } else {
      return loadModel(model)
    }
  })

  await Promise.all(all)
  initInfoBox()

  if (toDetach) {
    transformControls.detach()
    currentObject.value = null
  }

  if (props.option.models.length === 0) {
    const geometry = new THREE.BoxGeometry(8, 8, 8)
    const material = new THREE.MeshStandardMaterial({ color: 0x0088ff })
    const cube = new THREE.Mesh(geometry, material)
    cube.name = '默认模型'
    cube.userData.whole = cube
    scene.add(cube)
    modelObjs.push(cube)
  }
}
// 信息框
const initInfoBox = () => {
  cache.forEach(({ mesh }, name) => {
    if (props.infoData[name]) {
      const infoBoxDiv = document.createElement('div')
      infoBoxDiv.innerHTML = props.infoData[name]
      infoBoxDiv.style.position = 'relative'

      const infoBox = new CSS2DObject(infoBoxDiv)
      infoBox.position.set(0, 0, 0)
      infoBox.center.set(0, 0)
      infoBox.visible = false

      mesh.remove(...mesh.getObjectsByProperty('isCSS2DObject', true))
      mesh.add(infoBox)
      mesh.userData.infoBox = infoBox
    }
  })
}

watch(
  () => props.option.models,
  (newValue) => {
    initModels()
  },
  { deep: true }
)

watch(
  () => props.infoData,
  (newValue) => {
    initInfoBox()
  },
  { deep: true }
)

const addEventListener = () => {
  modelRef.value.addEventListener('mousemove', onMouseMove)
  modelRef.value.addEventListener('mouseup', onmouseup)
}

const removeEventListener = () => {
  modelRef.value.removeEventListener('mousemove', onMouseMove)
  modelRef.value.removeEventListener('mouseup', onmouseup)
}
// 帧处理
function animate() {
  const delta = clock.getDelta()

  orbitControls.update()

  for (let i = 0; i < mixers.length; i++) {
    mixers[i].update(delta)
  }

  renderer.render(scene, camera)
  infoBoxRenderer.render(scene, camera)
}

onMounted(() => {
  init()
  initModels()
  addEventListener()
  resizeObserver.observe(modelRef.value)
})

onBeforeUnmount(() => {
  resizeObserver.disconnect()
  removeEventListener()
})
</script>

<template>
  <div ref="modelRef" class="model" :class="{ pointer: pointer }">
    <!-- <div v-if="pointer" :style="{ left: `${title.x}px`, top: `${title.y}px` }" class="title">
      {{ title.value }}
    </div> -->
    <!-- 变换输入框 -->
    <div v-if="currentObject && editing" :data-update="updateTransformParams" class="transform-box">
      <ElTag type="primary" effect="dark">{{ currentObject.userData.model.name }}</ElTag>
      <ElInputNumber
        v-model="
          currentObject[
            option.transformControls.mode == 'translate'
              ? 'position'
              : option.transformControls.mode == 'rotate'
                ? 'quaternion'
                : 'scale'
          ].x
        "
        size="small"
        :controls="false"
        @change="updateModelMatrix"
      >
        <template #prefix>
          <span style="color: var(--el-color-primary)">x</span>
        </template>
      </ElInputNumber>
      <ElInputNumber
        v-model="
          currentObject[
            option.transformControls.mode == 'translate'
              ? 'position'
              : option.transformControls.mode == 'rotate'
                ? 'quaternion'
                : 'scale'
          ].y
        "
        size="small"
        :controls="false"
        @change="updateModelMatrix"
      >
        <template #prefix>
          <span style="color: var(--el-color-primary)">y</span>
        </template>
      </ElInputNumber>
      <ElInputNumber
        v-model="
          currentObject[
            option.transformControls.mode == 'translate'
              ? 'position'
              : option.transformControls.mode == 'rotate'
                ? 'quaternion'
                : 'scale'
          ].z
        "
        size="small"
        :controls="false"
        @change="updateModelMatrix"
      >
        <template #prefix>
          <span style="color: var(--el-color-primary)">z</span>
        </template>
      </ElInputNumber>
      <ElInputNumber
        v-if="option.transformControls.mode == 'rotate'"
        v-model="currentObject.quaternion.w"
        size="small"
        :controls="false"
        @change="updateModelMatrix"
      >
        <template #prefix>
          <span style="color: var(--el-color-primary)">w</span>
        </template>
      </ElInputNumber>
    </div>
  </div>
</template>

<style scoped>
.model {
  width: 100%;
  height: 100%;
  position: relative;
}

.pointer {
  cursor: pointer;
}

.title {
  user-select: none;
  color: #fff;
  background-color: rgb(0 0 0 / 0.8);
  padding: 5px 10px;
  border-radius: 5px;
  position: absolute;
}

.transform-box {
  position: absolute;
  right: 5px;
  bottom: 5px;
}
</style>
